# -*- coding: utf-8 -*-
# @Author  : CaoHan
# @Time    : 2023/12/30 11:18
import copy
import json
from collections import defaultdict
import numpy as np
import random
from simulator_150_50 import simulation
from tqdm import tqdm


class Individual(object):
    def __init__(self):
        self.solution = None
        self.objective = defaultdict()

        self.n = 0  # 解p被几个解所支配，是一个数值（左下部分点的个数）
        self.rank = 0  # 解所在第几层
        self.S = []  # 解p支配哪些解，是一个解集合（右上部分点的内容）
        self.distance = 0  # 拥挤度距离

    def bound_process(self, thre_1_min, thre_1_max, thre_2_max):
        """
        对解向量 solution 中的每个分量进行定义域判断，超过最大值，将其赋值为最大值；小于最小值，赋值为最小值
        """
        if self.solution[0] < thre_1_min:
            self.solution[0] = thre_1_min
        elif self.solution[0] > thre_1_max:
            self.solution[0] = thre_1_max

        if self.solution[1] < self.solution[0] + 10:
            self.solution[1] = self.solution[0] + 10
        elif self.solution[1] > thre_2_max:
            self.solution[1] = thre_2_max

    def calculate_objective(self, objective_fun):
        self.objective = objective_fun(self.solution)

    # 重载小于号“<”
    def __lt__(self, other):
        v1 = list(self.objective.values())
        v2 = list(other.objective.values())
        for i in range(len(v1)):
            if v1[i] > v2[i]:
                return 0  # 但凡有一个位置是 v1大于v2的 直接返回0,如果相等的话比较下一个目标值
        return 1


def main():
    # 初始化/参数设置
    generations = 10  # 迭代次数
    popnum = 10  # 种群大小
    eta = 1  # 变异分布参数，不需要了

    thre_1_min = 0
    thre_1_max = 10
    thre_2_max = 80
    objective_fun = func

    # 生成第一代种群
    P = []
    for i in range(popnum):
        P.append(Individual())
        thre_1 = random.uniform(0, thre_1_max)
        thre_2 = random.uniform(thre_1 + 10, thre_2_max)
        P[i].solution = np.array([thre_1, thre_2])
        P[i].bound_process(thre_1_min, thre_1_max, thre_2_max)  # 定义域越界处理
        P[i].calculate_objective(objective_fun)  # 计算目标函数值

    # 否 -> 非支配排序
    fast_non_dominated_sort(P)
    Q = make_new_pop(P, eta, objective_fun, thre_1_min, thre_1_max, thre_2_max)

    P_t = P  # 当前这一届的父代种群
    Q_t = Q  # 当前这一届的子代种群

    for gen_cur in tqdm(range(generations)):
        R_t = P_t + Q_t  # combine parent and offspring population
        F = fast_non_dominated_sort(R_t)

        P_n = []  # 即为P_t+1,表示下一届的父代
        i = 1
        while len(P_n) + len(F[i]) < popnum:  # until the parent population is filled
            crowding_distance_assignment(F[i])  # calculate crowding-distance in F_i
            P_n = P_n + F[i]  # include ith non dominated front in the parent pop
            i = i + 1  # check the next front for inclusion
        F[i].sort(key=lambda x: x.distance)  # sort in descending order using <n，因为本身就在同一层，所以相当于直接比拥挤距离
        P_n = P_n + F[i][:popnum - len(P_n)]
        Q_n = make_new_pop(P, eta, objective_fun, thre_1_min, thre_1_max,
                           thre_2_max)  # use selection,crossover and mutation to create a new population Q_n
        # 求得下一届的父代和子代成为当前届的父代和子代，，进入下一次迭代 《=》 t = t + 1
        P_t = P_n
        Q_t = Q_n
    R_t = P_t + Q_t  # combine parent and offspring population
    F = fast_non_dominated_sort(R_t)
    # save data
    solution = []
    X = []
    Y = []
    data_dict = {
        'solution': solution,
        'X': X,
        'Y': Y
    }
    for q in F[1]:
        solution.append(q.solution.tolist())
        X.append(1 - q.objective[1])
        Y.append(q.objective[2])
        with open('../data/150_50/ecsoa_nsga2_f1.json', mode='w') as file:
            json.dump(data_dict, file)
    return 0


def fast_non_dominated_sort(P):
    """
    非支配排序
    :param P: 种群 P
    :return F: F=(F_1, F_2, ...) 将种群 P 分为了不同的层， 返回值类型是dict，键为层号，值为 List 类型，存放着该层的个体
    """
    F = defaultdict(list)

    for p in P:
        p.S = []
        p.n = 0
        for q in P:
            if p < q:  # if p dominate q
                p.S.append(q)  # Add q to the set of solutions dominated by p
            elif q < p:
                p.n += 1  # Increment the domination counter of p
        if p.n == 0:
            p.rank = 1
            F[1].append(p)

    i = 1
    while F[i]:
        Q = []
        for p in F[i]:
            for q in p.S:
                q.n = q.n - 1
                if q.n == 0:
                    q.rank = i + 1
                    Q.append(q)
        i = i + 1
        F[i] = Q

    return F


def crowding_distance_assignment(L):
    """ 传进来的参数应该是L = F(i)，类型是List"""
    l = len(L)  # number of solution in F

    for i in range(l):
        L[i].distance = 0  # initialize distance

    for m in L[0].objective.keys():
        L.sort(key=lambda x: x.objective[m])  # sort using each objective value
        L[0].distance = float('inf')
        L[l - 1].distance = float('inf')  # so that boundary points are always selected

        # 排序是由小到大的，所以最大值和最小值分别是 L[l-1] 和 L[0]
        f_max = L[l - 1].objective[m]
        f_min = L[0].objective[m]

        # 当某一个目标方向上的最大值和最小值相同时，此时会发生除零错，这里采用异常处理机制来解决
        try:
            for i in range(1, l - 1):  # for all other points
                L[i].distance = L[i].distance + (L[i + 1].objective[m] - L[i - 1].objective[m]) / (f_max - f_min)
        except Exception:
            print(str(m) + "目标方向上，最大值为" + str(f_max) + "最小值为" + str(f_min))


def binary_tournament(ind1, ind2):
    """
    二元锦标赛
    :param ind1:个体1号
    :param ind2: 个体2号
    :return:返回较优的个体
    """
    if ind1.rank != ind2.rank:  # 如果两个个体有支配关系，即在两个不同的rank中，选择rank小的
        return ind1 if ind1.rank < ind2.rank else ind2
    elif ind1.distance != ind2.distance:  # 如果两个个体rank相同，比较拥挤度距离，选择拥挤读距离大的
        return ind1 if ind1.distance > ind2.distance else ind2
    else:  # 如果rank和拥挤度都相同，返回任意一个都可以
        return ind1


def make_new_pop(P, eta, objective_fun, thre_1_min, thre_1_max, thre_2_max):
    """
    use select,crossover and mutation to create a new population Q
    :param P: 父代种群
    :param eta: 变异分布参数，该值越大则产生的后代个体逼近父代的概率越大。Deb建议设为 1
    :param objective_fun: 目标函数
    :return Q : 子代种群
    """
    popnum = len(P)
    Q = []
    # binary tournament selection
    for i in range(int(popnum / 2)):
        # 从种群中随机选择两个个体，进行二元锦标赛，选择出一个 parent1
        i = random.randint(0, popnum - 1)
        j = random.randint(0, popnum - 1)
        parent1 = binary_tournament(P[i], P[j])

        # 从种群中随机选择两个个体，进行二元锦标赛，选择出一个 parent2
        i = random.randint(0, popnum - 1)
        j = random.randint(0, popnum - 1)
        parent2 = binary_tournament(P[i], P[j])

        while (parent1.solution == parent2.solution).all():  # 如果选择到的两个父代完全一样，则重选另一个
            i = random.randint(0, popnum - 1)
            j = random.randint(0, popnum - 1)
            parent2 = binary_tournament(P[i], P[j])

        # parent1 和 parent2 进行交叉，变异 产生 2 个子代
        Two_offspring = crossover_mutation(parent1, parent2, eta, objective_fun, thre_1_min, thre_1_max, thre_2_max)

        # 产生的子代进入子代种群
        Q.append(Two_offspring[0])
        Q.append(Two_offspring[1])
    return Q


def crossover_mutation(parent1, parent2, eta, objective_fun, thre_1_min, thre_1_max, thre_2_max):
    """
    交叉变异方式参考论文
    :param parent1: 父代1
    :param parent2: 父代2
    :param eta: 变异分布参数，该值越大则产生的后代个体逼近父代的概率越大。Deb建议设为 1
    :param bound_min: 定义域下限
    :param bound_max: 定义域上限
    :param objective_fun: 目标函数
    :return: 2 个子代
    """

    offspring1 = Individual()
    offspring2 = Individual()
    offspring1.solution = copy.deepcopy(parent1.solution)
    offspring2.solution = copy.deepcopy(parent2.solution)

    # 交叉
    if random.random() < 0.8:
        i = random.randint(0, 1)
        temp = offspring1.solution[i]
        offspring1.solution[i] = parent2.solution[i]
        offspring2.solution[i] = temp

    # 变异
    # 变异的时候只变异一个
    if random.random() < 0.4:
        offspring1.solution[0] = mutation(offspring1.solution[0], thre_1_min, thre_1_max, eta)
        offspring1.solution[1] = mutation(offspring1.solution[1], offspring1.solution[0] + 10, thre_2_max, eta)

    # 定义域越界处理
    offspring1.bound_process(thre_1_min, thre_1_max, thre_2_max)
    offspring2.bound_process(thre_1_min, thre_1_max, thre_2_max)

    # 计算目标函数值
    offspring1.calculate_objective(objective_fun)
    offspring2.calculate_objective(objective_fun)

    return [offspring1, offspring2]


def mutation(v, low, up, eta_m):
    delta1 = 1.0 * (v - low) / (up - low)
    delta2 = 1.0 * (up - v) / (up - low)
    u = random.random()
    mut_pow = 1.0 / (eta_m + 1.0)
    if u <= 0.5:
        xy = 1.0 - delta1
        val = 2.0 * u + (1.0 - 2.0 * u) * (xy ** (eta_m + 1.0))
        deltaq = val ** mut_pow - 1.0
    else:
        xy = 1.0 - delta2
        val = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (xy ** (eta_m + 1.0))
        deltaq = 1.0 - val ** mut_pow
    v = v + deltaq * (up - low)
    v = min(up, max(v, low))
    return v


def func(x):
    f = defaultdict(float)
    f1, f2 = simulation(x)
    f[1] = f1
    f[2] = f2
    return f


if __name__ == '__main__':
    main()
